<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebSocket</title>
  </head>
  <body>
    <h1>WebSocket Client</h1>
    <div id="messages"></div>
    <input type="text" id="messageInput" placeholder="Type a message" />
    <button onclick="sendMessage()">发送</button>
    <script>
      class Log {
        static console = true
        log(title, text) {
          if (!Log.console) return
          const color = '#0ea5e9'
          console.log(
            `%c ${title} %c ${text} %c`,
            `background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
            `border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
            'background:transparent',
          )
        }

        closeConsole() {
          Log.console = false
        }
      }
      class EventDispatcher extends Log {
        listeners = {}

        addEventListener(type, listener) {
          if (!this.listeners[type]) {
            this.listeners[type] = []
          }
          if (!this.listeners[type].includes(listener)) {
            this.listeners[type].push(listener)
          }
        }

        removeEventListener(type) {
          this.listeners[type] = []
        }

        dispatchEvent(type, data) {
          const listenerArray = this.listeners[type] || []
          if (listenerArray.length === 0) return
          listenerArray.forEach((listener) => {
            listener.call(this, data)
          })
        }
      }

      class WebSocketClient extends EventDispatcher {
        // #socket链接
        url = ''
        // #socket实例
        socket = null
        // #重连次数
        reconnectAttempts = 0
        // #最大重连数
        maxReconnectAttempts = 5
        // #重连间隔
        reconnectInterval = 10000 // 10 seconds
        // #发送心跳数据间隔
        heartbeatInterval = 1000 * 30
        // #计时器id
        heartbeatTimer = undefined
        // #彻底终止ws
        stopWs = false
        // *构造函数
        constructor(url) {
          super()
          this.url = url
        }

        // >生命周期钩子
        onopen(callBack) {
          this.addEventListener('open', callBack)
        }

        onmessage(callBack) {
          this.addEventListener('message', callBack)
        }

        onclose(callBack) {
          this.addEventListener('close', callBack)
        }

        onerror(callBack) {
          this.addEventListener('error', callBack)
        }

        // >消息发送
        send(message) {
          if (this.socket && this.socket.readyState === WebSocket.OPEN) {
            this.socket.send(message)
          } else {
            console.error('[WebSocket] 未连接')
          }
        }

        // !初始化连接
        connect() {
          if (this.reconnectAttempts === 0) {
            this.log('WebSocket', `初始化连接中...          ${this.url}`)
          }
          if (this.socket && this.socket.readyState === WebSocket.OPEN) {
            return
          }
          this.socket = new WebSocket(this.url)

          // !websocket连接成功
          this.socket.onopen = (event) => {
            this.stopWs = false
            // 重置重连尝试成功连接
            this.reconnectAttempts = 0
            // 在连接成功时停止当前的心跳检测并重新启动
            this.startHeartbeat()
            this.log(
              'WebSocket',
              `连接成功,等待服务端数据推送[onopen]...     ${this.url}`,
            )
            this.dispatchEvent('open', event)
          }

          this.socket.onmessage = (event) => {
            this.dispatchEvent('message', event)
            this.startHeartbeat()
          }

          this.socket.onclose = (event) => {
            if (this.reconnectAttempts === 0) {
              this.log('WebSocket', `连接断开[onclose]...    ${this.url}`)
            }
            if (!this.stopWs) {
              this.handleReconnect()
            }
            this.dispatchEvent('close', event)
          }

          this.socket.onerror = (event) => {
            if (this.reconnectAttempts === 0) {
              this.log('WebSocket', `连接异常[onerror]...    ${this.url}`)
            }
            this.closeHeartbeat()
            this.dispatchEvent('error', event)
          }
        }

        // > 断网重连逻辑
        handleReconnect() {
          if (this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnectAttempts++
            this.log(
              'WebSocket',
              `尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})       ${this.url}`,
            )
            setTimeout(() => {
              this.connect()
            }, this.reconnectInterval)
          } else {
            this.closeHeartbeat()
            this.log('WebSocket', `最大重连失败，终止重连: ${this.url}`)
          }
        }

        // >关闭连接
        close() {
          if (this.socket) {
            this.stopWs = true
            this.socket.close()
            this.socket = null
            this.removeEventListener('open')
            this.removeEventListener('message')
            this.removeEventListener('close')
            this.removeEventListener('error')
          }
          this.closeHeartbeat()
        }

        // >开始心跳检测 -> 定时发送心跳消息
        startHeartbeat() {
          if (this.stopWs) return
          if (this.heartbeatTimer) {
            this.closeHeartbeat()
          }
          this.heartbeatTimer = setInterval(() => {
            if (this.socket) {
              this.socket.send(JSON.stringify({ type: 'heartBeat', data: {} }))
              this.log('WebSocket', '送心跳数据...')
            } else {
              console.error('[WebSocket] 未连接')
            }
          }, this.heartbeatInterval)
        }

        // >关闭心跳
        closeHeartbeat() {
          clearInterval(this.heartbeatTimer)
          this.heartbeatTimer = undefined
        }
      }

      const ws = new WebSocketClient('ws://localhost:3200')

      // 连接
      ws.connect()
      // 同原生方法
      ws.onclose((e) => {
        console.log('连接关闭', e)
      })
      // 同原生方法
      ws.onerror((e) => {
        console.log('连接异常', e)
      })
      // 同原生方法
      ws.onmessage((e) => {
        // 同原生方法
        console.log('Message from server: ', event, event.data)
        document.getElementById('messages').innerHTML +=
          `<p>Server: ${event.data}</p>`
      })
      // 同原生方法
      ws.onopen(() => {
        console.log('连接服务器成功!')
        document.getElementById('messages').innerHTML +=
          '<p>连接服务器成功!</p>'
      })

      function sendMessage() {
        const input = document.getElementById('messageInput')
        const message = input.value
        ws.send(message)
        document.getElementById('messages').innerHTML +=
          `<p>You: ${message}</p>`
        input.value = ''
      }
    </script>
  </body>
</html>
